Repository navigation
fix: split CORS policy across public, credentialed, and admin routes - #316
Conversation
Closes Divinity-Labs#268 - Implement dynamic CORS delegator classifying routes into public, credentialed, and admin tiers - Keep public reads open to any origin with explicit credentials: false - Restrict credentialed routes (/usage/me and requests carrying auth credentials) to CORS_ALLOWED_ORIGINS with credentials: true - Close operator admin endpoints (/admin/*) against cross-origin browser requests by default - Document route security matrix and CORS policies in docs/routes.md and README.md - Add unit and integration tests covering allowed and refused origins on each class
|
@bamiebot-maker Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
One line from green. The drift guard is the only thing failing: Add it to Splitting CORS across public, credentialed and admin surfaces is the right shape — a single permissive policy in front of |
|
Hi @Miracle656, thanks for the review! I have added \CORS_PUBLIC_BLOCKED_ORIGINS\ to .env.example\ with documentation describing its format (comma-separated origins) and behavior (empty/unset permits all public cross-origin requests). \envDrift.test.ts\ and all 20 CORS tests are now green. |
|
Merged as The best idea in the PR is classifying on the request, not only on the path: const authHeader = headers['authorization']
const cookieHeader = headers['cookie']
if (authHeader || cookieHeader) return 'credentialed'
const requestHeaders = headers['access-control-request-headers']
if (typeof requestHeaders === 'string') {
const lower = requestHeaders.toLowerCase()
if (lower.includes('authorization') || lower.includes('cookie')) return 'credentialed'
}Checking the preflight's Merged locally; one conflict in Four changes on top. 1.
|
The versioning design is kept as submitted: routes stay mounted once at their
bare path, `rewriteUrl` resolves `/v1/...` onto them, and the unversioned alias
answers identically with Deprecation/Link headers. Four things had to be put
back before it could land.
**The spec dropped 17 routes.** openapi.yaml is hand-maintained (only
openapi.json is generated from it), and the committed file documented 26 paths
against main's 42. /book, /correlation, /discovery/search, /dispersion (both
forms), /impact, /live, /movers, /price/{asset}/fiat, /rates, /rates/{asset},
/ready, /snapshot, /tvl, /usage, /venues and /venues/registry were documented
nowhere — not under /v1, not bare — so they would have vanished from the
published contract and from the generated client. The spec is now main's,
with every path key moved to its /v1 form and the versioning paragraph from
this branch kept. /rates was also left unversioned, which the branch's own
new coverage test forbids.
**src/app.ts reverted shipped work.** It replaced the structured logger and
genReqId with `logger: { level: 'warn' }`, dropped the setErrorHandler that
routes 5xx through sendInternalError, dropped registerETag, and replaced the
per-route CORS delegator (#268/#316) with `origin: true` — allow-all. The
route-registration imports were gone while the calls remained, which is what
CI's typecheck was failing on. app.ts is now main's, with only the three
versioning touches reapplied: the import, `rewriteUrl`, and
registerApiVersioning after compress.
`exposedHeaders: ['Deprecation', 'Link']` moved into createCorsDelegator, so
every policy branch exposes them and none can forget; without it a browser
client cannot read the deprecation warning at all.
**README lost its ENDPOINTS:END marker**, so the generated-section contract
broke. Restored from main and regenerated; the table now carries 43 versioned
rows.
**Three test files assumed bare paths.** rest-route resolves the documented URL
through rewriteVersionedApiUrl instead of looking it up in the route table
(where /v1 never appears, by design). liveSpecValidation's spec keys are
versioned, leaving the JSON-pointer assertions on the response body alone.
resolveTestRequest normalises with toBarePath, since its query-parameter and
request-body tables are keyed on the bare route — /basket, /screener, /graphql,
/verify, /settle and /webhooks silently stopped matching.
Also restored isUndocumentableOperation in the method-coverage test: the branch
removed the only call but kept the allow-list, which would have reported
QUERY /graphql as undocumented.
openapi.json, README and the TypeScript client regenerated. tsc clean, 1,690
tests pass.
Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Summary
Splits the blanket \origin: true\ CORS configuration into three explicit security tiers:
Also documents the full route classification matrix in \docs/routes.md\ and adds comprehensive test coverage in \src/tests/cors.test.ts\ for allowed and refused origins across all classes.
Related issue
Closes #268
Type of change
Checklist
px tsc --noEmit\ passes
pm run build\ passes